'''
Created on May 19, 2013

@author: john
'''

from netifaces import interfaces, ifaddresses, AF_INET
import psutil
from psutil._error import NoSuchProcess, AccessDenied
import socket
from datetime import datetime, timedelta
import time
import re
from fdb import services
from uptime import uptime

class ServerError(Exception):
    pass

class Server(object):

    def __init__(self, host, user, password):
        '''
        Constructor
        '''
        self.host = host
        self.user = user
        self.password = password
        self.localStats = None

        # if it's a local instance, setup some tracking variables       
        if LocalStats.isLocalInstance(host):
            self.isLocalhost = True
            self.localStats = LocalStats()
        else:
            self.isLocalhost = False
                
        # connection to the services API
        self.__connection = None
       
        self.attachmentCount = None
        self.__prevAttachmentCount = None
        self.attachmentsPerMin = '?'
        self.__databaseNames = set()
        self.databases = []
        self.databaseCount = None
        self.__lastPollTime = None
    def poll(self):
         
        # if we're not connected to the services API, connect now
        if not self.__connection:
            self.__connection = services.connect(self.host, self.user, self.password)
        
        
        # if we're running locally, get some extra info like memory and CPU use
        if self.isLocalhost:
            self.localStats.poll()
        
        currentTime = time.time()
        self.attachmentCount = self.__connection.get_connection_count()
        
        if self.__prevAttachmentCount and self.__lastPollTime:
            duration = currentTime - self.__lastPollTime
            self.attachmentsPerMin = (self.attachmentCount - self.__prevAttachmentCount) / (duration * 60.0)
        
        attachedDatabaseNames = self.__connection.get_attached_database_names()
        self.databaseCount = len(attachedDatabaseNames)
        
        # loop over the names of the attached databases
        for databaseName in attachedDatabaseNames:

            # if we've never seen this database, create a new Database to keep track of it
            if databaseName not in self.__databaseNames:
                # each database uses the same services connection as Server
                newDatabase = Database(databaseName, self.host, self.user, self.password, self.__connection)
                self.databases.append(newDatabase);
                self.__databaseNames.add(databaseName)
        
        # poll all of the databases that have ever been attached
        for database in self.databases:
            database.poll()
        
        # save some stuff for next time
        self.__prevAttachmentCount = self.attachmentCount
        self.__lastPollTime = currentTime
    
    def disconnect(self):
        for database in self.databases:
            database.disconnect()
        
        if self.__connection:
            self.__connection.close()
            self.__connection = None

class Database(object):
    '''
    classdocs
    '''
    _integerRe = re.compile(r'^.*\s(\d+)\s?$')
    _attributesRe = re.compile(r'Attributes\s*(.*)')

    
    def __init__(self, path, host, user, password, servicesConnection):
        '''
        Constructor
        '''
        self.path = path
#         self.host = host
#         self.user = user
#        self.password = password
       
        self._servicesConnection = servicesConnection
        
        # tracking variables
        self._prevNextTxId = None
        self._prevNextAttachmentId = None
        self._lastPollTime = None
        
        # user displayable variables
        self.gap = '?'
        self.oitGap = '?'
        self.ostGap = '?'
        self.oatGap = '?'
        self.attachmentCount = '?'
        self.attachmentsPerMin = '?'
        self.dialect = '?'
        self.attributes = '?'
        self.pageSize = '?'
        self.transactionsPerMinute = '?'
    
    def _extractInteger(self, line):
        match = re.match(Database._integerRe, line)
        if match:
            return int(match.group(1))
        else:
            return None

    def _extractAttributes(self, line):
        attributesMatch = re.search(Database._attributesRe, line)
        if attributesMatch:
            self.attributes = attributesMatch.group(1)
        
    def poll(self):
        self._servicesConnection.get_statistics(self.path, show_only_db_log_pages = False, show_only_db_header_pages = True, show_user_data_pages = False, show_user_index_pages = False, show_system_tables_and_indexes = False, show_record_versions = False, callback = None)
        lines = self._servicesConnection.readlines()
        currentTime = time.time();
        
        for line in lines:
            if 'Page size' in line:
                self.pageSize = self._extractInteger(line)
            elif 'Oldest transaction' in line:
                oit = self._extractInteger(line)
            elif 'Oldest active' in line:
                oat = self._extractInteger(line)
            elif 'Oldest snapshot' in line:
                ost = self._extractInteger(line)
            elif 'Next transaction' in line:
                nextTxId = self._extractInteger(line)
            elif 'Next attachment ID' in line:
                nextAttachmentId = self._extractInteger(line)
            elif 'Database dialect' in line:
                self.dialect = self._extractInteger(line)
            elif 'Attributes' in line:
                self._extractAttributes(line)
    
        if self._prevNextTxId and self._lastPollTime:
            elapsed = currentTime - self._lastPollTime
            if elapsed > 0.0:
                transactionCount = nextTxId - self._prevNextTxId
                txPerMinFloat = (float(transactionCount) / elapsed) * 60.0
                self.transactionsPerMinute = round(txPerMinFloat, 1)
            
        self.oitGap = nextTxId - oit
        self.oatGap = nextTxId - oat
        self.ostGap = nextTxId - ost
        self.gap = min(self.oitGap, self.oatGap, self.ostGap)
    
        self._prevNextTxId = nextTxId
        self._prevNextAttachmentId = nextAttachmentId
        self._lastPollTime = currentTime;

    def disconnect(self):
        pass;

class LocalStatsError(Exception):
    pass

class LocalStats(object):
    __firebirdProcessNames = ['fb_smp_server', 'fb_inet_server']
    __removeFractionalSecondsRe = re.compile(r'\..*$')
    
    # get a list of the IP addresses, hostnames, and FQDN that the LocalStats is known by
    @staticmethod
    def __getLocalhostAliases():
        aliases = []
        for interface in interfaces():
            addresses = ifaddresses(interface)
            if AF_INET in addresses:
                for link in addresses[AF_INET]:
                    aliases.append(link['addr'].lower())
        
        aliases.append('localhost')
        aliases.append(socket.gethostname().lower())
        aliases.append(socket.getfqdn().lower())
        return aliases
    
    @classmethod
    def isLocalInstance(cls, hostname):
        return hostname.lower() in cls.__getLocalhostAliases();
    
    def __durationAsString(self, startTimeSeconds):
        dateDiff = timedelta(seconds = startTimeSeconds)
        
        # remove fractions of a second.  for example, '6:23:08.3823' => '6:23:08'
        return re.sub(LocalStats.__removeFractionalSecondsRe, '', str(dateDiff))
    
    def __init__(self):
        self.uptime =  self.__durationAsString(uptime())
        self.firebirdUptime = '?'
        self.memoryPercent = '?'
        
        # textual description of bytes read/written, calculated based on the current value
        # minus the value since the last poll
        self.ioRead = '?'
        self.ioWrite = '?'
        self.__pid = None
        
        self.__fbStartTime = None
        
        # the value of the previous bytes read/written counters at the time of the last poll
        self.__prevBytesRead = None
        self.__prevBytesWritten = None
        
    def __findFirebirdPid(self):
        # find all the Firebird processes running on this box
        firebirdProcessIds = []
        pidList = psutil.get_pid_list()
        for pid in pidList:
            process = psutil.Process(pid)
            if process.name in LocalStats.__firebirdProcessNames:
                # optimistically assume that this is the process we're interested in and grab
                # its start time.  we'll throw an exception later if isn't.
                self.__fbStartTime = process.create_time
                firebirdProcessIds.append(pid)
        
        badPidException = None
        
        # if there's more than one Firebird instance or we couldn't find an instance,
        # then throw an exception
        if len(firebirdProcessIds) > 1:
            badPidException = LocalStatsError("more than one Firebird server was found")
        elif len(firebirdProcessIds) == 0:
            badPidException = LocalStatsError("no instance of Firebird was found")
        
        if badPidException:
            raise(badPidException)
        
        self.__pid = firebirdProcessIds[0]
        
    def poll(self):
      
        # if we haven't already figured out what the PID for the local Firebird instance is,
        # figure it out now
        if not self.__pid:
            self.__findFirebirdPid()
        
        currentTime = time.time()
        
        # update our human readable uptime duration
        self.uptime =  self.__durationAsString(uptime())
        self.firebirdUptime = self.__durationAsString(currentTime - self.__fbStartTime)
        
        try:
            process = psutil.Process(self.__pid)
            
            try:
                # basic per-process memory stats are known to work on Linux and Mac OS X
                self.memoryPercent = process.get_memory_percent()
                self.rss = process.get_memory_info().rss
    
                # psutil doesn't support per-process IO counters on Mac OS X                    
                if hasattr(process, 'get_io_counters'):
                    ioInfo = process.get_io_counters()
                    
                    
                    # if we had a previous reading, compute the difference between this reading and the previous
                    # reading as a human readable string 
                    if self.__prevBytesRead != None and self.__prevBytesWritten != None and self._lastPollTime:
                        elapsed = currentTime - self.__lastPollTime
                        if elapsed > 0.0:
                            self.ioRead = str((ioInfo.read_bytes - self.__prevBytesRead) / elapsed) + ' bytes/sec'
                            self.ioWrite = str((ioInfo.write_bytes - self.__prevBytesWritten) / elapsed) + ' bytes/sec'
                    
                    # save the current reading for next time 
                    self.__prevBytesRead = ioInfo.read_bytes
                    self.__prevBytesWritten = ioInfo.write_bytes
                 
                    # save the current time for the next poll
                    self._lastPollTime = currentTime
            except AccessDenied:
                self.memoryPercent = '?'
                self.rss = '?'
                
        # the Firebird process died
        except NoSuchProcess:
            localInstanceFailed = LocalStatsError('the local Firebird instance is no longer running')
            raise(localInstanceFailed)
    